/**
 *  
 */
package com.googlecode.cswish.model;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.googlecode.cswish.util.OgnlExUtil;

/**
 * PageElement repository
 * <p>
 * It store all the element information to a list, each name of PageElement should be different
 * 
 * <p>Structure:
 * <pre>
 * 1. Element Tree:
 * PageElements
 *     |-- PageElement
 *     |-- PageElement
 *     |-- PageElements
 * 	       |-- PageElement
 * 	       |-- ...
 * 
 * 3. Form names:
 * model
 * model.element1
 * model.element2
 * model.element3.element31
 * 
 * 4. relationship
 * element.name == @cs.element.name == uiBean.name == @s.xxx.name
 * element.name != element.shortName
 * 
 * </pre>
 * 
 * @author Jerry.Feng Date: 2008-4-8
 */
public class PageElements extends PageElement {
	
	private static final Log logger = LogFactory.getLog(PageElements.class);
	
	private static final PageElements BLANK = new PageElements(null);
	
	// request action name
	private String action;
	
	// request method name
	private String method;
	
	// submit method name
	private String targetAction;
	
	// multipart/form-data
	private String enctype;
	
	// page style
	private String css;
	
	// child elements
	private List<PageElement> childElements;
	
	//////////////////////////// Cache ////////////////////////////////////////
	// it include current child and all the child's child
	private Map<String, PageElement> allElementsCache;
	
	// the element count which displayed in one line
	private int columnCount;
	
	private boolean isArray;
	
	public PageElements(OgnlExUtil ognlExUtil) {
		super(ognlExUtil);
		
		reset();
	}
	
	public void reset() {
		this.childElements = new ArrayList<PageElement>();
		this.allElementsCache = new LinkedHashMap<String, PageElement>();
		this.columnCount = 2;
	}
	
	@Override
	public boolean isElement() {
		return false;
	}

	public String getAction() {
		return action;
	}

	public void setAction(String action) {
		this.action = action;
	}
	
	/**
	 * Call another method
	 * 
	 * @param method
	 * @return
	 */
	public String action(String method) {
		return action.substring(0, action.length() - this.method.length()) + method;
	}

	public String getMethod() {
		return method;
	}

	public void setMethod(String method) {
		this.method = method;
	}
	
	public String getTargetAction() {
		return targetAction;
	}

	public void setTargetAction(String targetMethod) {
		this.targetAction = targetMethod;
	}
	
	public String getEnctype() {
		return enctype;
	}

	public void setEnctype(String enctype) {
		this.enctype = enctype;
	}

	/**
	 * Hidden element isn't visible
	 *  
	 * @param target
	 * @param child element.shortName & element.visible are required
	 * @return
	 */	
	public PageElement addElement(PageElement childElement) {
		if (childElement instanceof PageElements) {
			PageElements child = (PageElements) childElement;
			
			PageElement existed = child.getElement(getName(), false);
			if (existed != null) {	// Right?
				logger.error("Found dead cycle reference:" + getName() + "-->" + child.getName());
				return null;
			}
		}
		
		// FIXME: How to process the duplicate name? use id as the map key?
		childElement.setParent(this);
		childElements.add(childElement);
		return allElementsCache.put(childElement.getName(), childElement);
	}
	
	public void removeElement(PageElement childElement) {
		if (childElement == null && childElement != BLANK) {
			return ;
		}
		
		boolean existed = childElements.remove(childElement);
		if (existed) {
			allElementsCache.remove(childElement);
		}
	}
	
	/**
	 * It supports the nested name.
	 * 
	 * search order: name as short name --> formated name as short name --> 
	 * skip the matched prefix and call child 
	 * 
	 * @param name: fullName/shortName
	 * @param showWarning
	 * @return
	 */
	public PageElement getElement(String childElementName, boolean showWarning) {
		PageElement element = allElementsCache.get(childElementName);
		if (showWarning && element == null) {
			logger.error("Can't find element:" + childElementName);
		}
		return element;
	}	

	/**
	 * Return a non-null object to avoid the unnecessary null object check
	 * @param name
	 * @return
	 */
	public PageElement safeElement(String name) {
		PageElement element = getElement(name, false);
		if (element == null) {
			element = BLANK;
		}
		return element;
	}
	
	public PageElement getElement(String name) {
		PageElement element = getElement(name, true);
		return element;
	}
	
	public PageElement findContainerByType(String type) {
		for (PageElement pageElement : allElementsCache.values()) {
			if (pageElement instanceof PageElements) {
				if (type.equals(pageElement.getType())) {
					return pageElement;
				}
			}
		}
		return null;
	}
	
	public List<PageElement> getElements() {
		return childElements;
	}
	
	public PageElements getParent() {
		return (PageElements) parent;
	}

	public int getColumnCount() {
		return columnCount;
	}

	public void setColumnCount(int columnCount) {
		this.columnCount = columnCount;
	}
	
	/**
	 * @return visible elements
	 */
	public List<PageElement> getVisibleElements() {
		List<PageElement> list = new ArrayList<PageElement>();
		for (PageElement pageElement : childElements) {
			if (pageElement.isVisible()) {
				list.add(pageElement);
			}
		}
		return list;
	}
	
	/**
	 * @return hidden elements
	 */
	public List<PageElement> getHiddenElements() {
		List<PageElement> list = new ArrayList<PageElement>();
		for (PageElement pageElement : childElements) {
			if (!pageElement.isVisible()) {
				list.add(pageElement);
			}
		}
		return list;
	}

	public String getCss() {
		return css;
	}
	
	public void setCss(String css) {
		this.css = css;
	}
	
	public boolean isArray() {
		return isArray;
	}

	public void setArray(boolean isArray) {
		this.isArray = isArray;
	}

	public void refreshCache() {
		allElementsCache.clear();

		for (PageElement childElement : childElements) {
			if (childElement instanceof PageElements) {
				PageElements pageElements = (PageElements) childElement;
				pageElements.refreshCache();
				
				allElementsCache.putAll(pageElements.allElementsCache);
			}
			allElementsCache.put(childElement.getName(), childElement);
		}
	}
	
	public Map<String, PageElement> getAllElements() {
		return allElementsCache;
	}
	
	public List<String> getModelFields() {
		List<String> modelFields = new ArrayList<String>();
		for (PageElement pageElement : getAllElements().values()) {
			if (pageElement.isModelField()) {
				modelFields.add(pageElement.getName());
			}
		}
		return modelFields;
	}

	public String toString() {
		StringBuilder sb = new StringBuilder();
		int index = type.lastIndexOf('.');
		index = type.lastIndexOf('.', index - 1);
		sb.append("{shortName=").append(getShortName());
		sb.append(" type=").append(type.substring(index + 1));
		sb.append(" name=").append(getName());
		sb.append(" size=").append(childElements.size()).append('}');
		return sb.toString();
	}
	
	public void parseTo(StringBuilder out) {
		parseElements(this, out, 0);
	}
	
	private void parseElements(PageElements elements, StringBuilder out, int level) {
		if (elements.getParent() == null) {
			out.append("<@s.form id='").append(elements.getId()).append("Form' ");
			out.append("action='").append(elements.getTargetAction()).append('\'');
			if (elements.getEnctype() != null) {
				out.append(" enctype='").append(elements.getEnctype()).append('\'');
			}
			out.append(" validate=false>\n");
		} else {
			parseElement(elements, out, level, false);
		}
		
		int childLevel = level + 1;
		for (PageElement element :  elements.getElements()) {
			if (element instanceof PageElements) {
				parseElements((PageElements)element, out, childLevel);
			} else {
				parseElement(element, out, childLevel, true);
			}
		}
		
		if (elements.getParent() == null) {
			out.append("</@s.form>\n");
		} else {
			for (int i = 0; i < level; i++) {
				out.append('\t');
			}
			out.append("</@cs.element>\n");
		}
	}
	
	private void parseElement(PageElement element, StringBuilder out, int level, boolean hasEnd) {
		for (int i = 0; i < level; i++) {
			out.append('\t');
		}
		out.append("<@cs.element type='").append(element.getType()).append("' name='").append(element.getName()).append('\'');
		appendNotNullValue(out, "id", element.getId());
		appendNotNullValue(out, "label", element.getLabel());
		appendNotNullValue(out, "theme", element.getTheme());
		appendNotNullValue(out, "colspan", element.getColspan());
		Set<Entry> entries = element.getExtraParams().entrySet();
		for (Entry entry : entries) {
			appendNotNullValue(out, entry.getKey(), entry.getValue());
		}
		appendNotNullValue(out, "index", element.getIndex());
		appendNotNullValue(out, "length", element.getLength());
		appendNotNullValue(out, "size", element.getSize());
		appendNotNullValue(out, "textModel", element.getTextModel());
		appendNotNullValue(out, "value", element.getValue());
		out.append(">");
		
		if (hasEnd) {
			out.append("</@cs.element>\n");
		} else {
			out.append('\n');
		}
	}
	
	
	private void appendNotNullValue(StringBuilder out, Object name, Object value) {
		if (value != null) {
			if (value instanceof String) {
				out.append(' ').append(name).append("='").append(value).append('\'');
			} else if (value instanceof Number) {
				int v = Integer.parseInt(value.toString());
				if (v != 0) {
					out.append(' ').append(name).append("=").append(value);
				}
			} else {
				out.append(' ').append(name).append("=").append(value);
			}
		}
	}
	
	public String debug() {
		if (logger.isDebugEnabled()) {
			StringBuilder sb = new StringBuilder();
			sb.append("Debug info, element structure for ").append(getName()).append('\n');
			for (PageElement theElement : getVisibleElements()) {
				sb.append(theElement).append('\n');
			}
			logger.debug(sb.toString());
		}
		return "";
	}
	
	// pageElements can't contain self-reference
	public PageElements clone() {
		PageElements cloned = new PageElements(ognlExUtil);
		cloneTo(cloned);
		return cloned;
	}
	
	public void cloneTo(PageElements cloned) {
		super.cloneTo(cloned);
		
		cloned.setAction(getAction());
		cloned.setMethod(getMethod());
		cloned.setTargetAction(getTargetAction());
		cloned.setParent(getParent());
		cloned.setArray(isArray());
		cloned.setColumnCount(getColumnCount());
		cloned.setEnctype(getEnctype());
		if (label != null) {
			cloned.setLabel(label);
		}

		for (PageElement element : childElements) {
			PageElement child = element.clone();
			cloned.addElement(child);
		}
	}
}